import html
import os
import time
import requests, re
from typing import Sized
import aiohttp, asyncio
from tqdm import tqdm
from pathlib import Path
from asyncio import Lock
import hashlib

#全局参数
params={'id':0,'imei_ip':'2937357107','teeny_mode':0}
headers={'app-version':'77800','platform':'android','reg':'0','AUTHORIZATION':'','application-id':'com.****.reader','net-env':'1','channel':'unknown','qm-params':''}

def get_book_id(url : str) -> Sized | None:
        """提取数字作为 bookid"""
        matches = re.findall(r"\d+", url)
        return max(matches, key=len) if matches else None

def clean_content_with_linebreaks(raw_content: str) -> str:
    """
    清洗内容：保留回车换行，移除HTML标签、Unicode转义、乱码和多余空格
    """
    # 步骤1：解码Unicode转义
    decoded_content = html.unescape(raw_content)

    # 步骤2：将<p>标签转换为换行符，保留段落结构
    # 将</p>和<p>标签替换为换行符
    paragraph_content = re.sub(r'</p>', '\n', decoded_content)
    paragraph_content = re.sub(r'<p[^>]*>', '', paragraph_content)

    # 步骤3：移除剩余的HTML标签
    no_tag_content = re.sub(r'<[^>]+>', '', paragraph_content, flags=re.S)

    # 步骤4：清理乱码字符
    clean_text = re.sub(
        r'[^\u4e00-\u9fa5a-zA-Z0-9\s，。！？；""''（）【】\n\t]',
        '',
        no_tag_content
    )

    # 步骤5：清理每行首尾空格
    clean_text = re.sub(r'^\s+|\s+$', '', clean_text, flags=re.M)

    # 步骤6：清理行内多余空格
    clean_text = re.sub(r'[ \t]+', ' ', clean_text)

    # 步骤7：优化换行（合并连续空行）
    clean_text = re.sub(r'\n+', '\n', clean_text).strip()

    return clean_text

def generate_signed_params(params: dict, sign_key: str = "d3dGiJc651gSQ8w1") -> dict:
    """
    给一个字典 params 生成签名 sign 并返回新字典

    签名规则：
    1. 将 params 所有 key 按字母升序排序
    2. 拼接 key=value 的字符串
    3. 在末尾加上 sign_key
    4. 对拼接后的字符串做 MD5，生成 sign
    5. 将 sign 添加到原字典返回

    Args:
        params (dict): 请求参数字典
        sign_key (str): 签名密钥

    Returns:
        dict: 带 sign 的新字典
    """
    # 1. 按 key 排序拼接字符串
    sorted_keys = sorted(params.keys())
    concatenated = ''.join(f"{k}={params[k]}" for k in sorted_keys)

    # 2. 加上密钥
    concatenated += sign_key

    # 3. 生成 MD5
    sign = hashlib.md5(concatenated.encode('utf-8')).hexdigest()

    # 4. 返回新的字典
    signed_params = params.copy()
    signed_params['sign'] = sign
    return signed_params

class Cat:
    def __init__(self):
        self.base_url_info = "https://api-bc.wtzw.com/api/v4/book"
        self.base_url  ="https://api-ks.wtzw.com/api/v1/chapter"

    def get_book_info(self, bookid:str) -> dict | None:
        url = self.base_url_info + "/detail"
        original_params = params
        original_params['id'] = int(bookid)
        params_sign = generate_signed_params(original_params)
        headers_sign = generate_signed_params(headers)
        for retry in range(3):
            try:
                response = requests.get(url, headers=headers_sign, params = params_sign, timeout = 10)
                if response.status_code == 200:
                    data = response.json().get("data", None).get("book", None)
                    return {"title": data["title"],
                            "author": data["author"],
                            "chapternum": data["chapters"],
                            "abstract": data["intro"]
                            }
            except:
                continue
        return None

    def get_chapter_list(self, bookid:str) -> list | None:
        url = self.base_url + "/chapter-list"
        for retry in range(3):
            try:
                headers_sign = generate_signed_params(headers)
                original_params = params
                original_params['id'] = int(bookid)
                params_sign = generate_signed_params(original_params)
                response = requests.get(url, headers=headers_sign, params = params_sign, timeout = 10)
                if response.status_code == 200:
                    data = response.json().get("data", None)
                    item_data_list = data.get("chapter_lists", None)
                    if item_data_list is not None:
                        return [{
                            "num" : item["index"],
                            "item_id" : item["id"],
                            "title" : item["title"]
                        }
                        for item in item_data_list]
            except:
                continue
        return None

async def get_single_chapter(item : dict, total_progress, tqdm_lock, bid) -> dict | None:
    num = item["num"]
    item_id = item["item_id"]
    url = f"https://fanqie.hnxianxin.cn/qimao.php?bookId={bid}&chapterId={item_id}"
    async with aiohttp.ClientSession() as session:
        for retry in range(3):
            try:
                async with session.get(url, headers=headers) as response:
                    if response.status == 200:
                        content = (await response.json()).get("data", None).get("content", None)
                        if content is not None:
                            # 更新进度条
                            async with tqdm_lock:
                                total_progress.update(1)
                            return {
                                "num" : num,
                                "content" : clean_content_with_linebreaks(content)
                            }
            except:
                continue


async def get_all_chapters(chapter_list: list, bookname: str, bookid) -> list | None:
    results = []
    tqdm_lock = Lock()
    total_progress = tqdm(
        total=len(chapter_list),
        desc=f"下载《{bookname}》",
        leave=False,
        position=1,
        dynamic_ncols=True
    )

    # 每10个章节为一组进行处理
    for i in range(0, len(chapter_list), 10):
        # 获取当前批次的10个章节
        batch = chapter_list[i:i + 10]

        # 并发获取当前批次的章节内容
        batch_results = await asyncio.gather(*(get_single_chapter(item=item, total_progress=total_progress, tqdm_lock=tqdm_lock, bid=bookid) for item in batch))

        # 过滤掉None结果并添加到总结果中
        valid_results = [result for result in batch_results if result is not None]
        results.extend(valid_results)

        # 如果不是最后一批，暂停1秒
        if i + 10 < len(chapter_list) and i % 5 != 0:
            await asyncio.sleep(0.5)

    # 关闭进度条
    total_progress.close()

    return results

def main(fanqie:Cat, book_id) -> None:
    """

    :rtype: None
    """
    book_info = cat.get_book_info(book_id)
    if book_info is None:
        raise "书本信息为空，请重试"
    chapter_list = cat.get_chapter_list(book_id)
    if chapter_list is None:
        raise "章节列表为空，请重试"
    contents = asyncio.run(get_all_chapters(chapter_list = chapter_list, bookid=book_id, bookname=book_info["title"]))
    if contents is None:
        raise "获取章节失败"
    path_utf = f"data/utf/"
    path_gb = f"data/gb/"
    os.makedirs(path_utf, exist_ok=True)
    os.makedirs(path_gb, exist_ok=True)
    book_path_utf = path_utf + book_info["title"] + ".txt"
    book_path_gb = path_gb + book_info["title"] + ".txt"
    if os.path.exists(book_path_utf):
        os.remove(book_path_utf)
    if os.path.exists(book_path_gb):
        os.remove(book_path_gb)
    with open(book_path_utf, "w", encoding="utf-8") as f:
        f.write(
            f"书名：{book_info['title']}\n\n"
            f"作者：{book_info['author']}\n\n"
            f"书籍简介：\n{book_info['abstract']}\n\n"
        )
        for chapter in contents:
            f.write(f"{chapter['content']}\n\n")
    with open(book_path_gb, "w", encoding="gb18030") as f:
        f.write(
            f"书名：{book_info['title']}\n\n"
            f"作者：{book_info['author']}\n\n"
            f"书籍简介：\n{book_info['abstract']}"
        )
        for chapter in contents:
            f.write(f"{chapter['content']}\n\n")
    time.sleep(1)
    return book_info["title"]

if __name__ == "__main__":
    cat = Cat()
    judge = ""
    while judge != "q" and judge != "Q":
        text = input("请输入url/待下载的小说url:")
        if Path(text).is_file():
            with open(text, "r", encoding="utf-8") as f:
                urls = f.readlines()
                with tqdm(
                        total=len(urls),
                        desc="总进度:",
                        leave=False,
                        position=0,
                        dynamic_ncols=True
                ) as pbar:
                    fail = 0
                    fail_url = []
                    for url in urls:
                        try:
                            book_id = get_book_id(url)
                            if book_id is not None:
                                main(cat, book_id)
                            else:
                                fail += 1
                                fail_url.append(url)
                        finally:
                            pbar.update(1)
                print(f"一共{len(urls) - fail}下载完成")
                if fail > 0:
                    print(f"{fail}下载失败,失败的url如下:")
                    for url in fail_url:
                        print(url)
        else:
            try:
                book_id = get_book_id(text)
                if book_id is not None:
                    bookname = main(cat, book_id)
                    print(f"下载《{bookname}》完成")
            except:
                print("请检查输入内容")
        judge = input("输入q/Q退出,输入其他继续")